package com.xdb.sqlparser.parser;

import static com.xdb.sqlparser.parser.SQLCharTypes.isFirstIdentifierChar;
import static com.xdb.sqlparser.parser.SQLCharTypes.isIdentifierChar;
import static com.xdb.sqlparser.parser.SQLCharTypes.isWhitespace;
import static com.xdb.sqlparser.parser.SQLLayoutCharacters.EOI;
import static com.xdb.sqlparser.parser.SQLToken.COLONCOLON;
import static com.xdb.sqlparser.parser.SQLToken.COLONEQ;
import static com.xdb.sqlparser.parser.SQLToken.COMMA;
import static com.xdb.sqlparser.parser.SQLToken.EOF;
import static com.xdb.sqlparser.parser.SQLToken.ERROR;
import static com.xdb.sqlparser.parser.SQLToken.LBRACE;
import static com.xdb.sqlparser.parser.SQLToken.LBRACKET;
import static com.xdb.sqlparser.parser.SQLToken.LITERAL_ALIAS;
import static com.xdb.sqlparser.parser.SQLToken.LITERAL_CHARS;
import static com.xdb.sqlparser.parser.SQLToken.LPAREN;
import static com.xdb.sqlparser.parser.SQLToken.RBRACE;
import static com.xdb.sqlparser.parser.SQLToken.RBRACKET;
import static com.xdb.sqlparser.parser.SQLToken.RPAREN;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;

import com.xdb.sqlparser.exception.NotAllowCommentException;
import com.xdb.sqlparser.exception.ParserException;

public class SQLLexer {

	protected final String text;
	protected int pos;
	protected int mark;

	protected char ch;

	protected char[] buf;
	protected int bufPos;

	protected SQLToken token;

	protected SQLKeywords keywods = SQLKeywords.DEFAULT_KEYWORDS;

	protected String stringVal;

	protected boolean skipComment = true;

	private SavePoint savePoint = null;

	private boolean allowComment = true;

	private int varIndex = -1;

	protected CommentHandler commentHandler;

	protected boolean hasComment = false;

	protected boolean endOfComment = false;

	public SQLLexer(String input) {
		this(input, true);
	}

	public CommentHandler getCommentHandler() {
		return commentHandler;
	}

	public void setCommentHandler(CommentHandler commentHandler) {
		this.commentHandler = commentHandler;
	}

	public final char charAt(int index) {
		if (index >= text.length()) {
			return EOI;
		}

		return text.charAt(index);
	}

	public final String addSymbol() {
		return subString(mark, bufPos);
	}

	public final String subString(int offset, int count) {
		return text.substring(offset, offset + count);
	}

	protected void initBuff(int size) {
		if (buf == null) {
			if (size < 32) {
				buf = new char[32];
			} else {
				buf = new char[size + 32];
			}
		} else if (buf.length < size) {
			buf = Arrays.copyOf(buf, size);
		}
	}

	public void arraycopy(int srcPos, char[] dest, int destPos, int length) {
		text.getChars(srcPos, srcPos + length, dest, destPos);
	}

	public boolean isAllowComment() {
		return allowComment;
	}

	public void setAllowComment(boolean allowComment) {
		this.allowComment = allowComment;
	}

	public int nextVarIndex() {
		return ++varIndex;
	}

	private static class SavePoint {

		int bp;
		int sp;
		int np;
		char ch;
		SQLToken token;
	}

	public SQLKeywords getKeywods() {
		return keywods;
	}

	public void mark() {
		SavePoint savePoint = new SavePoint();
		savePoint.bp = pos;
		savePoint.sp = bufPos;
		savePoint.np = mark;
		savePoint.ch = ch;
		savePoint.token = token;
		this.savePoint = savePoint;
	}

	public void reset() {
		this.pos = savePoint.bp;
		this.bufPos = savePoint.sp;
		this.mark = savePoint.np;
		this.ch = savePoint.ch;
		this.token = savePoint.token;
	}

	public SQLLexer(String input, boolean skipComment) {
		this.skipComment = skipComment;

		this.text = input;
		this.pos = -1;

		scanChar();
	}

	public SQLLexer(char[] input, int inputLength, boolean skipComment) {
		this(new String(input, 0, inputLength), skipComment);
	}

	protected final void scanChar() {
		ch = charAt(++pos);
	}

	protected void unscan() {
		ch = charAt(--pos);
	}

	public boolean isEOF() {
		return pos >= text.length();
	}

	/**
	 * Report an error at the given position using the provided arguments.
	 */
	protected void lexError(String key, Object... args) {
		token = ERROR;
	}

	/**
	 * Return the current token, set by nextToken().
	 */
	public final SQLToken token() {
		return token;
	}

	public String info() {
		return this.token + " " + this.stringVal();
	}

	public final void nextTokenComma() {
		if (ch == ' ') {
			scanChar();
		}

		if (ch == ',' || ch == '，') {
			scanChar();
			token = COMMA;
			return;
		}

		if (ch == ')') {
			scanChar();
			token = RPAREN;
			return;
		}

		nextToken();
	}

	public final void nextTokenLParen() {
		if (ch == ' ') {
			scanChar();
		}

		if (ch == '(') {
			scanChar();
			token = LPAREN;
			return;
		}
		nextToken();
	}

	public final void nextTokenValue() {
		if (ch == ' ') {
			scanChar();
		}

		if (ch == '\'') {
			bufPos = 0;
			scanString();
			return;
		}

		if (ch >= '0' && ch <= '9') {
			bufPos = 0;
			scanNumber();
			return;
		}

		if (ch == '?') {
			scanChar();
			token = SQLToken.QUES;
			return;
		}

		if (isFirstIdentifierChar(ch) && ch != 'N') {
			scanIdentifier();
			return;
		}

		nextToken();
	}
	/**
	 * 读取下一个符号
	 */
	public final void nextToken() {
		bufPos = 0;

		for (;;) {
			//如果当前的字符是空白字符，则扫描以一个字符
			if (isWhitespace(ch)) {
				scanChar();
				continue;
			}
			//如果当前的字符是$，下一个字符是{,也就是${
			if (ch == '$' && charAt(pos + 1) == '{') {
				scanVariable();
				return;
			}
			//如果第一个标识符
			if (isFirstIdentifierChar(ch)) {
				if (ch == 'N') {
					if (charAt(pos + 1) == '\'') {
						++pos;
						ch = '\'';
						scanString();
						token = SQLToken.LITERAL_NCHARS;
						return;
					}
				}

				scanIdentifier();
				return;
			}

			switch (ch) {
				case '0' :
					if (charAt(pos + 1) == 'x') {
						scanChar();
						scanChar();
						scanHexaDecimal();
					} else {
						scanNumber();
					}
					return;
				case '1' :
				case '2' :
				case '3' :
				case '4' :
				case '5' :
				case '6' :
				case '7' :
				case '8' :
				case '9' :
					scanNumber();
					return;
				case ',' :
				case '，' :
					scanChar();
					token = COMMA;
					return;
				case '(' :
					scanChar();
					token = LPAREN;
					return;
				case ')' :
					scanChar();
					token = RPAREN;
					return;
				case '[' :
					scanLBracket();
					return;
				case ']' :
					scanChar();
					token = RBRACKET;
					return;
				case '{' :
					scanChar();
					token = LBRACE;
					return;
				case '}' :
					scanChar();
					token = RBRACE;
					return;
				case ':' :
					scanChar();
					if (ch == '=') {
						scanChar();
						token = COLONEQ;
					} else if (ch == ':') {
						scanChar();
						token = COLONCOLON;
					} else {
						if (isDigit(ch)) {
							unscan();
							scanVariable();
						} else {
							unscan();
							scanVariable();
						}
					}
					return;
				case '#' :
					scanSharp();
					if ((token() == SQLToken.LINE_COMMENT || token() == SQLToken.MULTI_LINE_COMMENT) && skipComment) {
						bufPos = 0;
						continue;
					}
					return;
				case '.' :
					scanChar();
					if (isDigit(ch) && !isFirstIdentifierChar(charAt(pos - 2))) {
						unscan();
						scanNumber();
						return;
					} else if (ch == '.') {
						scanChar();
						if (ch == '.') {
							scanChar();
							token = SQLToken.DOTDOTDOT;
						} else {
							token = SQLToken.DOTDOT;
						}
					} else {
						token = SQLToken.DOT;
					}
					return;
				case '\'' :
					scanString();
					return;
				case '\"' :
					scanAlias();
					return;
				case '*' :
					scanChar();
					token = SQLToken.STAR;
					return;
				case '?' :
					scanChar();
					token = SQLToken.QUES;
					return;
				case ';' :
					scanChar();
					token = SQLToken.SEMI;
					return;
				case '`' :
					throw new ParserException("TODO"); // TODO
				case '@' :
					scanVariable();
					return;
				case '-' :
					int subNextChar = charAt(pos + 1);
					if (subNextChar == '-') {
						scanComment();
						if ((token() == SQLToken.LINE_COMMENT || token() == SQLToken.MULTI_LINE_COMMENT) && skipComment) {
							bufPos = 0;
							continue;
						}
					} else {
						scanOperator();
					}
					return;
				case '/' :
					int nextChar = charAt(pos + 1);
					if (nextChar == '/' || nextChar == '*') {
						scanComment();
						if ((token() == SQLToken.LINE_COMMENT || token() == SQLToken.MULTI_LINE_COMMENT) && skipComment) {
							bufPos = 0;
							continue;
						}
					} else {
						token = SQLToken.SLASH;
						scanChar();
					}
					return;
				default :
					if (Character.isLetter(ch)) {
						scanIdentifier();
						return;
					}

					if (isOperator(ch)) {
						scanOperator();
						return;
					}

					// QS_TODO ?
					if (isEOF()) { // JLS
						token = EOF;
					} else {
						lexError("illegal.char", String.valueOf((int) ch));
						scanChar();
					}

					return;
			}
		}

	}

	protected void scanLBracket() {
		scanChar();
		token = LBRACKET;
	}

	private final void scanOperator() {
		switch (ch) {
			case '+' :
				scanChar();
				token = SQLToken.PLUS;
				break;
			case '-' :
				scanChar();
				token = SQLToken.SUB;
				break;
			case '*' :
				scanChar();
				token = SQLToken.STAR;
				break;
			case '/' :
				scanChar();
				token = SQLToken.SLASH;
				break;
			case '&' :
				scanChar();
				if (ch == '&') {
					scanChar();
					token = SQLToken.AMPAMP;
				} else {
					token = SQLToken.AMP;
				}
				break;
			case '|' :
				scanChar();
				if (ch == '|') {
					scanChar();
					if (ch == '/') {
						scanChar();
						token = SQLToken.BARBARSLASH;
					} else {
						token = SQLToken.BARBAR;
					}
				} else if (ch == '/') {
					scanChar();
					token = SQLToken.BARSLASH;
				} else {
					token = SQLToken.BAR;
				}
				break;
			case '^' :
				scanChar();
				token = SQLToken.CARET;
				break;
			case '%' :
				scanChar();
				token = SQLToken.PERCENT;
				break;
			case '=' :
				scanChar();
				if (ch == '=') {
					scanChar();
					token = SQLToken.EQEQ;
				} else {
					token = SQLToken.EQ;
				}
				break;
			case '>' :
				scanChar();
				if (ch == '=') {
					scanChar();
					token = SQLToken.GTEQ;
				} else if (ch == '>') {
					scanChar();
					token = SQLToken.GTGT;
				} else {
					token = SQLToken.GT;
				}
				break;
			case '<' :
				scanChar();
				if (ch == '=') {
					scanChar();
					if (ch == '>') {
						token = SQLToken.LTEQGT;
						scanChar();
					} else {
						token = SQLToken.LTEQ;
					}
				} else if (ch == '>') {
					scanChar();
					token = SQLToken.LTGT;
				} else if (ch == '<') {
					scanChar();
					token = SQLToken.LTLT;
				} else {
					token = SQLToken.LT;
				}
				break;
			case '!' :
				scanChar();
				if (ch == '=') {
					scanChar();
					token = SQLToken.BANGEQ;
				} else if (ch == '>') {
					scanChar();
					token = SQLToken.BANGGT;
				} else if (ch == '<') {
					scanChar();
					token = SQLToken.BANGLT;
				} else if (ch == '!') {
					scanChar();
					token = SQLToken.BANGBANG; // postsql
				} else {
					token = SQLToken.BANG;
				}
				break;
			case '?' :
				scanChar();
				token = SQLToken.QUES;
				break;
			case '~' :
				scanChar();
				token = SQLToken.TILDE;
				break;
			default :
				throw new ParserException("TODO");
		}
	}

	protected void scanString() {
		mark = pos;
		boolean hasSpecial = false;

		for (;;) {
			if (isEOF()) {
				lexError("unclosed.str.lit");
				return;
			}

			ch = charAt(++pos);

			if (ch == '\'') {
				scanChar();
				if (ch != '\'') {
					token = LITERAL_CHARS;
					break;
				} else {
					if (!hasSpecial) {
						initBuff(bufPos);
						arraycopy(mark + 1, buf, 0, bufPos);
						hasSpecial = true;
					}
					putChar('\'');
					continue;
				}
			}

			if (!hasSpecial) {
				bufPos++;
				continue;
			}

			if (bufPos == buf.length) {
				putChar(ch);
			} else {
				buf[bufPos++] = ch;
			}
		}

		if (!hasSpecial) {
			stringVal = subString(mark + 1, bufPos);
		} else {
			stringVal = new String(buf, 0, bufPos);
		}
	}

	private final void scanAlias() {
		mark = pos;

		if (buf == null) {
			buf = new char[32];
		}

		boolean hasSpecial = false;
		for (;;) {
			if (isEOF()) {
				lexError("unclosed.str.lit");
				return;
			}

			ch = charAt(++pos);

			if (ch == '\\') {
				hasSpecial = true;
				continue;
			}

			if (ch == '\"' && charAt(pos - 1) != '\\') {
				scanChar();
				token = LITERAL_ALIAS;
				break;
			}

			if (bufPos == buf.length) {
				putChar(ch);
			} else {
				buf[bufPos++] = ch;
			}
		}

		if (!hasSpecial) {
			stringVal = subString(mark + 1, bufPos);
		} else {
			stringVal = new String(buf, 0, bufPos);
		}

		// stringVal = subString(mark + 1, bufPos);
	}

	public void scanSharp() {
		scanVariable();
	}
	/**
	 * 扫描变量
	 */
	public void scanVariable() {
		//如果当前字符不是@,:,#,$抛出异常
		if (ch != '@' && ch != ':' && ch != '#' && ch != '$') {
			throw new ParserException("illegal variable");
		}

		mark = pos;
		bufPos = 1;
		char ch;

		if (charAt(pos + 1) == '@') {
			ch = charAt(++pos);

			bufPos++;
		} else if (charAt(pos + 1) == '{') {
			pos++;
			bufPos++;

			for (;;) {
				ch = charAt(++pos);

				if (ch == '}') {
					break;
				}

				bufPos++;
				continue;
			}

			if (ch != '}') {
				throw new ParserException("syntax error");
			}
			++pos;
			bufPos++;

			this.ch = charAt(pos);

			stringVal = addSymbol();
			token = SQLToken.VARIANT;
			return;
		}

		for (;;) {
			ch = charAt(++pos);

			if (!isIdentifierChar(ch)) {
				break;
			}

			bufPos++;
			continue;
		}

		this.ch = charAt(pos);

		stringVal = addSymbol();
		token = SQLToken.VARIANT;
	}

	public void scanComment() {
		if (!allowComment) {
			throw new NotAllowCommentException();
		}

		if ((ch == '/' && charAt(pos + 1) == '/') || (ch == '-' && charAt(pos + 1) == '-')) {
			scanSingleLineComment();
		} else if (ch == '/' && charAt(pos + 1) == '*') {
			scanMultiLineComment();
		} else {
			throw new IllegalStateException();
		}
	}

	private void scanMultiLineComment() {
		scanChar();
		scanChar();
		mark = pos;
		bufPos = 0;

		for (;;) {
			if (ch == '*' && charAt(pos + 1) == '/') {
				scanChar();
				scanChar();
				break;
			}

			// multiline comment结束符错误
			if (ch == EOI) {
				throw new ParserException("unterminated /* comment.");
			}
			scanChar();
			bufPos++;
		}

		stringVal = subString(mark, bufPos);
		token = SQLToken.MULTI_LINE_COMMENT;
		hasComment = true;
	}

	private void scanSingleLineComment() {
		scanChar();
		scanChar();
		mark = pos;
		bufPos = 0;

		for (;;) {
			if (ch == '\r') {
				if (charAt(pos + 1) == '\n') {
					scanChar();
					break;
				}
				bufPos++;
				break;
			}

			if (ch == '\n') {
				scanChar();
				break;
			}

			// single line comment结束符错误
			if (ch == EOI) {
				throw new ParserException("syntax error at end of input.");
			}

			scanChar();
			bufPos++;
		}

		stringVal = subString(mark, bufPos);
		token = SQLToken.LINE_COMMENT;
		hasComment = true;
	}

	public void scanIdentifier() {
		final char first = ch;

		final boolean firstFlag = isFirstIdentifierChar(first);
		if (!firstFlag) {
			throw new ParserException("illegal identifier");
		}

		mark = pos;
		bufPos = 1;
		char ch;
		for (;;) {
			ch = charAt(++pos);

			if (!isIdentifierChar(ch)) {
				break;
			}

			bufPos++;
			continue;
		}

		this.ch = charAt(pos);

		stringVal = addSymbol();
		SQLToken tok = keywods.getKeyword(stringVal);
		if (tok != null) {
			token = tok;
		} else {
			token = SQLToken.IDENTIFIER;
		}
	}

	public void scanNumber() {
		mark = pos;

		if (ch == '-') {
			bufPos++;
			ch = charAt(++pos);
		}

		for (;;) {
			if (ch >= '0' && ch <= '9') {
				bufPos++;
			} else {
				break;
			}
			ch = charAt(++pos);
		}

		boolean isDouble = false;

		if (ch == '.') {
			if (charAt(pos + 1) == '.') {
				token = SQLToken.LITERAL_INT;
				return;
			}
			bufPos++;
			ch = charAt(++pos);
			isDouble = true;

			for (;;) {
				if (ch >= '0' && ch <= '9') {
					bufPos++;
				} else {
					break;
				}
				ch = charAt(++pos);
			}
		}

		if (ch == 'e' || ch == 'E') {
			bufPos++;
			ch = charAt(++pos);

			if (ch == '+' || ch == '-') {
				bufPos++;
				ch = charAt(++pos);
			}

			for (;;) {
				if (ch >= '0' && ch <= '9') {
					bufPos++;
				} else {
					break;
				}
				ch = charAt(++pos);
			}

			isDouble = true;
		}

		if (isDouble) {
			token = SQLToken.LITERAL_FLOAT;
		} else {
			token = SQLToken.LITERAL_INT;
		}
	}

	public void scanHexaDecimal() {
		mark = pos;

		if (ch == '-') {
			bufPos++;
			ch = charAt(++pos);
		}

		for (;;) {
			if (SQLCharTypes.isHex(ch)) {
				bufPos++;
			} else {
				break;
			}
			ch = charAt(++pos);
		}

		token = SQLToken.LITERAL_HEX;
	}

	public String hexString() {
		return subString(mark, bufPos);
	}

	public final boolean isDigit(char ch) {
		return ch >= '0' && ch <= '9';
	}

	/**
	 * Append a character to sbuf.
	 */
	protected final void putChar(char ch) {
		if (bufPos == buf.length) {
			char[] newsbuf = new char[buf.length * 2];
			System.arraycopy(buf, 0, newsbuf, 0, buf.length);
			buf = newsbuf;
		}
		buf[bufPos++] = ch;
	}

	/**
	 * Return the current token's position: a 0-based offset from beginning of
	 * the raw input stream (before unicode translation)
	 */
	public final int pos() {
		return pos;
	}

	/**
	 * The value of a literal token, recorded as a string. For integers, leading
	 * 0x and 'l' suffixes are suppressed.
	 */
	public final String stringVal() {
		return stringVal;
	}

	private boolean isOperator(char ch) {
		switch (ch) {
			case '!' :
			case '%' :
			case '&' :
			case '*' :
			case '+' :
			case '-' :
			case '<' :
			case '=' :
			case '>' :
			case '^' :
			case '|' :
			case '~' :
			case ';' :
				return true;
			default :
				return false;
		}
	}

	private static final long MULTMIN_RADIX_TEN = Long.MIN_VALUE / 10;
	private static final long N_MULTMAX_RADIX_TEN = -Long.MAX_VALUE / 10;

	private final static int[] digits = new int[(int) '9' + 1];

	static {
		for (int i = '0'; i <= '9'; ++i) {
			digits[i] = i - '0';
		}
	}

	// QS_TODO negative number is invisible for lexer
	public Number integerValue() {
		long result = 0;
		boolean negative = false;
		int i = mark, max = mark + bufPos;
		long limit;
		long multmin;
		int digit;

		if (charAt(mark) == '-') {
			negative = true;
			limit = Long.MIN_VALUE;
			i++;
		} else {
			limit = -Long.MAX_VALUE;
		}
		multmin = negative ? MULTMIN_RADIX_TEN : N_MULTMAX_RADIX_TEN;
		if (i < max) {
			digit = digits[charAt(i++)];
			result = -digit;
		}
		while (i < max) {
			// Accumulating negatively avoids surprises near MAX_VALUE
			digit = digits[charAt(i++)];
			if (result < multmin) {
				return new BigInteger(numberString());
			}
			result *= 10;
			if (result < limit + digit) {
				return new BigInteger(numberString());
			}
			result -= digit;
		}

		if (negative) {
			if (i > mark + 1) {
				if (result >= Integer.MIN_VALUE) {
					return (int) result;
				}
				return result;
			} else { /* Only got "-" */
				throw new NumberFormatException(numberString());
			}
		} else {
			result = -result;
			if (result <= Integer.MAX_VALUE) {
				return (int) result;
			}
			return result;
		}
	}

	public int bp() {
		return this.pos;
	}

	public char current() {
		return this.ch;
	}

	public void reset(int mark, char markChar, SQLToken token) {
		this.pos = mark;
		this.ch = markChar;
		this.token = token;
	}

	public final String numberString() {
		return subString(mark, bufPos);
	}

	public BigDecimal decimalValue() {
		return new BigDecimal(subString(mark, bufPos).toCharArray());
	}

	public static interface CommentHandler {

		boolean handle(SQLToken lastToken, String comment);
	}

	public boolean isHasComment() {
		return hasComment;
	}

	public void skipToEOF() {
		pos = text.length();
		this.token = SQLToken.EOF;
	}

	public boolean isEndOfComment() {
		return endOfComment;
	}

	protected boolean isSafeComment(String comment) {
		if (comment == null) {
			return true;
		}
		comment = comment.toLowerCase();
		if (comment.indexOf("select") != -1 //
				|| comment.indexOf("delete") != -1 //
				|| comment.indexOf("insert") != -1 //
				|| comment.indexOf("update") != -1 //
				|| comment.indexOf("into") != -1 //
				|| comment.indexOf("where") != -1 //
				|| comment.indexOf("or") != -1 //
				|| comment.indexOf("and") != -1 //
				|| comment.indexOf("union") != -1 //
				|| comment.indexOf('\'') != -1 //
				|| comment.indexOf('=') != -1 //
				|| comment.indexOf('>') != -1 //
				|| comment.indexOf('<') != -1 //
				|| comment.indexOf('&') != -1 //
				|| comment.indexOf('|') != -1 //
				|| comment.indexOf('^') != -1 //
		) {
			return false;
		}
		return true;
	}

}
